package com.mayee.me.gateway.route;

import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import com.alibaba.nacos.api.exception.NacosException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.filter.FilterDefinition;
import org.springframework.cloud.gateway.handler.predicate.PredicateDefinition;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionRepository;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.CollectionUtils;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

/**
 * @description:
 * @author: mayee
 * @create: 2020-12-26 19:16
 **/
@Slf4j
@Configuration
@RefreshScope
public class NacosDynamicRoute implements ApplicationEventPublisherAware, RouteDefinitionRepository {

    @Autowired
    private RouteConfiguration routeConfiguration;
    @Autowired
    private NacosConfigManager nacosConfigManager;

    private ApplicationEventPublisher applicationEventPublisher;

    private final AtomicInteger loadRedis = new AtomicInteger();
    private final AtomicInteger loadNacos = new AtomicInteger();

    private static final String CACHE_KEY_PREFIX = "route";

    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }

    @Override
    public Flux<RouteDefinition> getRouteDefinitions() {
        List<RouteDefinition> routeDefinitions = new ArrayList<>();
        String cacheKey = String.format("%s-%s-%s", CACHE_KEY_PREFIX, routeConfiguration.getRouteDataId(), routeConfiguration.getRouteDataGroup());
        //todo 从缓存中加载路由
        if (StringUtils.isEmpty(cacheKey)) {
            RouteGroupEntity config = new RouteGroupEntity();
            List<RouteEntity> routeList = config.getRouteList();
            if (routeConfiguration.isShowRouteRefresh()) {
                //todo
                log.info("todo 日志输出");
            }
            if (!CollectionUtils.isEmpty(routeList)) {
                return Flux.fromIterable(assembleRouteDefinition(config));
            }
        }

        try {
            ConfigService configService = nacosConfigManager.getConfigService();
            String configInfo = configService.getConfig(routeConfiguration.getRouteDataId(), routeConfiguration.getRouteDataGroup(), 1000);
            RouteGroupEntity config = JSON.parseObject(configInfo, RouteGroupEntity.class);
            if (Objects.isNull(config)) {
                log.error("No route config,DataID:{},DataGroup:{},load cout:{}", routeConfiguration.getRouteDataId(), routeConfiguration.getRouteDataGroup(), loadNacos.incrementAndGet());
                return Flux.fromIterable(routeDefinitions);
            }
            List<RouteEntity> routeList = config.getRouteList();
            if (CollectionUtils.isEmpty(routeList)) {
                log.error("No route config,DataID:{},DataGroup:{},load cout:{}", routeConfiguration.getRouteDataId(), routeConfiguration.getRouteDataGroup(), loadNacos.incrementAndGet());
                return Flux.fromIterable(routeDefinitions);
            }
            if (routeConfiguration.isShowRouteRefresh()) {
                log.info("Load route config from nacos,DataID:{},DataGroup:{},size:{},load cout:{}", routeConfiguration.getRouteDataId(), routeConfiguration.getRouteDataGroup(), routeList.size(), loadNacos.incrementAndGet());
            }
            // todo 存入缓存
            if (routeConfiguration.isShowRouteRefresh()) {
                log.info("Put route Cache:{},DataID:{},DataGroup:{},size:{}", cacheKey, routeConfiguration.getRouteDataId(), routeConfiguration.getRouteDataGroup(), routeList.size());
            }
            return Flux.fromIterable(assembleRouteDefinition(config));
        } catch (NacosException e) {
            log.error(e.getErrMsg());
        }
        return Flux.fromIterable(routeDefinitions);
    }

    @Bean
    public void refreshRouting() throws NacosException {
        ConfigService configService = nacosConfigManager.getConfigService();
        configService.getConfigAndSignListener(routeConfiguration.getRouteDataId(), routeConfiguration.getRouteDataGroup(), 1000, new Listener() {
            @Override
            public Executor getExecutor() {
                return null;
            }

            @Override
            public void receiveConfigInfo(String configInfo) {
                if (routeConfiguration.isShowRouteRefresh()) {
                    log.info("Receive Nacos route update event,DataID:{},DataGroup:{}", routeConfiguration.getRouteDataId(), routeConfiguration.getRouteDataGroup());
                }
                RouteGroupEntity config = JSON.parseObject(configInfo, RouteGroupEntity.class);
                //todo 存入缓存
                applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
            }
        });
    }

    @Override
    public Mono<Void> save(Mono<RouteDefinition> route) {
        return route.flatMap(r -> {
            if (StringUtils.isEmpty(r.getId())) {
                return Mono.error(new IllegalArgumentException("id may not be empty"));
            }
            applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
            return Mono.empty();
        });
    }

    @Override
    public Mono<Void> delete(Mono<String> routeId) {
        applicationEventPublisher.publishEvent(new RefreshRoutesEvent(this));
        return Mono.empty();
    }

    private List<RouteDefinition> assembleRouteDefinition(RouteGroupEntity config) {
        List<RouteDefinition> routeDefinitions = new ArrayList<>();
        List<RouteEntity> routes = config.getRouteList();
        if (CollectionUtils.isEmpty(routes)) {
            return routeDefinitions;
        }
        for (RouteEntity eachRoute : routes) {
            RouteDefinition definition = new RouteDefinition();
            definition.setId(eachRoute.getId());
            definition.setOrder(eachRoute.getOrder());

            List<PredicateDefinition> pdList = eachRoute.getPredicates().stream().map(predicateEntity -> {
                PredicateDefinition predicateDefinition = new PredicateDefinition(predicateEntity.toString());
                predicateDefinition.setName(predicateEntity.getName());
                if (routeConfiguration.isShowRouteRefresh()) {
                    log.info("Predicate:{},Order:{}", predicateEntity.toString(), eachRoute.getOrder());
                }
                return predicateDefinition;
            }).collect(Collectors.toList());
            definition.setPredicates(pdList);

            List<FilterDefinition> fdList = eachRoute.getFilters().stream().map(filterEntity -> {
                FilterDefinition filterDefinition = new FilterDefinition();
                filterDefinition.setArgs(filterEntity.getArgs());
                filterDefinition.setName(filterEntity.getName());
                return filterDefinition;
            }).collect(Collectors.toList());
            definition.setFilters(fdList);

            if (!CollectionUtils.isEmpty(eachRoute.getMetadata())) {
                definition.setMetadata(eachRoute.getMetadata());
            }
            URI uri = UriComponentsBuilder.fromUriString(eachRoute.getUri()).build().toUri();
            definition.setUri(uri);
            routeDefinitions.add(definition);
            if (routeConfiguration.isShowRouteRefresh()) {
                log.info("RouteDefinition:{}", definition.toString());
            }
        }
        return routeDefinitions;
    }
}
